Skip to main content

pacenotes_installer\engine\execute/
uninstall_package.rs

1/// Removes one package, putting back what each of its files replaced.
2pub fn uninstall_package(
3    run: &mut super::run::RunContext,
4    key: &str,
5    outcome: &mut super::Outcome,
6) -> Result<(), super::ExecuteError> {
7    let Some(entry) = run.ledger.get(key).cloned() else {
8        return Ok(());
9    };
10    // Only a write-ahead entry takes identical originals from the payload (see
11    // `put_back`), and it reads the payload once, not once for each of them.
12    let identical =
13        |file: &crate::ledger::LedgerFile| file.prior == crate::ledger::Prior::Identical;
14    let installed = entry.state == crate::ledger::EntryState::Installed;
15    let payload = if !installed && entry.files.values().any(identical) {
16        std::fs::read(run.cache.payload_path(&entry.sha256)).ok()
17    } else {
18        None
19    };
20    let archive = payload
21        .as_deref()
22        .and_then(|payload| crate::zip::Archive::open(payload).ok());
23    for file in entry.files.values() {
24        if let Some(reason) = put_back(run, key, &entry, file, archive.as_ref())? {
25            run.watcher
26                .log(&format!("{} {reason}; left alone", file.path));
27            outcome.left_alone.push(file.path.clone());
28        }
29    }
30    prune_empty_dirs(run.game_dir, &entry);
31    run.ledger
32        .remove(key)
33        .map_err(super::ExecuteError::ledger)?;
34    outcome.uninstalled.push(key.to_string());
35    run.watcher.log(&format!("removed {key}"));
36    Ok(())
37}
38
39/// Restores what was at the file's path before `key` wrote it, or returns why
40/// the file has to stay as it is.
41fn put_back(
42    run: &mut super::run::RunContext,
43    key: &str,
44    entry: &crate::ledger::LedgerEntry,
45    file: &crate::ledger::LedgerFile,
46    payload: Option<&crate::zip::Archive>,
47) -> Result<Option<String>, super::ExecuteError> {
48    let target = crate::relative_path::join(run.game_dir, &file.path);
49    if !target.exists() {
50        return Ok(None);
51    }
52    let others: Vec<&str> = run
53        .ledger
54        .owners_of(&file.path)
55        .into_iter()
56        .filter(|owner| *owner != key)
57        .collect();
58    if !others.is_empty() {
59        // It keeps the last writer's bytes, which only this line reveals.
60        let line = format!(
61            "{} is still used by {}; left in place",
62            file.path,
63            others.join(", ")
64        );
65        run.watcher.log(&line);
66        return Ok(None);
67    }
68    // A write-ahead entry never got its per-file hashes, and its files were ours either way.
69    if entry.state == crate::ledger::EntryState::Installed {
70        match crate::sha256::hex_digest_of_file(&target) {
71            Ok(found) if found == file.sha256 => {}
72            Ok(_) => return Ok(Some("has changed since it was installed".into())),
73            Err(e) => return Ok(Some(format!("could not be read ({e})"))),
74        }
75    }
76
77    match &file.prior {
78        crate::ledger::Prior::Absent => super::game_files::remove_file(&target)?,
79        crate::ledger::Prior::Backup { point } => {
80            let copy = super::restore_point::saved_copy(run.game_dir, point, &file.path);
81            if !copy.is_file() {
82                return Ok(Some(
83                    "cannot be put back (its saved copy is missing)".into(),
84                ));
85            }
86            super::game_files::copy_file(&copy, &target)?;
87        }
88        // The hash above proved the file holds what was written, which for an
89        // identical one is what was here before: it is the original already.
90        crate::ledger::Prior::Identical if entry.state == crate::ledger::EntryState::Installed => {}
91        crate::ledger::Prior::Identical => {
92            let original = payload.and_then(|archive| archive.read(&file.path).ok());
93            let Some(bytes) = original else {
94                return Ok(Some(
95                    "was already on this computer before it was installed, and the package \
96                     it came from is no longer cached"
97                        .into(),
98                ));
99            };
100            super::game_files::write_atomic(&target, &bytes)?;
101        }
102    }
103    Ok(None)
104}
105
106/// Removes the directories the uninstall emptied, deepest first, up to but not
107/// including the game folder.
108fn prune_empty_dirs(game_dir: &std::path::Path, entry: &crate::ledger::LedgerEntry) {
109    let mut dirs: Vec<std::path::PathBuf> = entry
110        .files
111        .values()
112        .filter_map(|file| {
113            crate::relative_path::join(game_dir, &file.path)
114                .parent()
115                .map(std::path::Path::to_path_buf)
116        })
117        .collect();
118    dirs.sort();
119    dirs.dedup();
120    dirs.sort_by_key(|dir| std::cmp::Reverse(dir.components().count()));
121    for dir in dirs {
122        let mut at = dir.as_path();
123        while at.starts_with(game_dir) && at != game_dir {
124            // Fails on a directory that is not empty, which is where pruning stops.
125            if std::fs::remove_dir(at).is_err() {
126                break;
127            }
128            match at.parent() {
129                Some(parent) => at = parent,
130                None => break,
131            }
132        }
133    }
134}